
            <!DOCTYPE html>
            <html lang="en">
            <head>
                <meta charset="UTF-8">
                <title>docker 容器如何使用tini 作为启动进程并 清理、管理 Docker 容器僵死进程</title>
            </head>
            <body>
            <a href="https://andyoung.blog.csdn.net">原作者博客</a>
            <div id="content_views" class="markdown_views prism-atom-one-light">
                    <svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
                        <path stroke-linecap="round" d="M5,0 0,2.5 5,5z" id="raphael-marker-block" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path>
                    </svg>
                    <p>一般的docker镜像为了节省空间，通常是没有安装systemd或者sysvint这类初始化系统的进程。一旦容器的起始进程不稳定将会产生大量的僵尸进程，影响宿主系统的运行。</p> 
<h3><a id="init_4"></a><strong>缺少init的容器</strong></h3> 
<p>init系统有以下几个特点：</p> 
<ul><li>它是系统的第一个进程，负责产生其他所有用户进程。</li><li>init 以守护进程方式存在，是所有其他进程的祖先。</li><li>它主要负责：</li></ul> 
<p>1.启动守护进程</p> 
<p>2.回收孤儿进程</p> 
<p>3.将操作系统信号转发给子进程</p> 
<p>以下dockerfile为例：</p> 
<pre><code class="prism language-text">FROM nginx

ENTRYPOINT ["nginx", "-c"]
CMD ["/etc/nginx/nginx.conf"]
</code></pre> 
<p>当docker容器启动时，<strong>PID 1即容器启动程序将会是nginx, 只要这个程序停止，容器就会跟住停止</strong>。由于nginx 不具备init 上述的功能，PID 1是无法回收异常退出进程，异常退出的进程变成僵尸进程，继续占用系统资源。</p> 
<p>当多个容器运行在一个宿主机上的时候,为了避免一个容器消耗完我们整个宿主机进程号资源，docker会配置PID CGROUP来限制每个容器的最大进程数目。也就是说，进程数目在每个容器中也是有限的，是一种很宝贵的资源。（例如：Linux 机器上的进程总数目是有限制，如果进程数据过多，比如你想ssh登录到机器上就不行 ）</p> 
<h3><a id="_tini__35"></a><strong>如何使用 tini 初始化系統</strong></h3> 
<p>tini 是一套更简单的 init 系统，专门用来执行一个子程序(spawn a single child)，并等待子程序结束，即便子程序已经变成僵尸程序也能捕捉到，同时也能转送 Signal 给子程序。如果你使用docker来跑容器，可以非常简便的在<strong>docker run</strong>的时候用**–init<strong>参数，就会自动注入tini程式 (</strong>/sbin/docker-init**) 到容器中，并且自动取代<strong>ENTRYPOINT</strong>设定，让原本的程式直接跑在 tini程序底下。</p> 
<p>dockerfile如下：</p> 
<pre><code class="prism language-text">FROM nginx

RUN export TINI_VERSION=0.9.0 &amp;&amp; \
    export TINI_SHA=fa23d1e20732501c3bb8eeeca423c89ac80ed452 &amp;&amp; \
    curl -fsSL https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini-static -o /bin/tini &amp;&amp; \
    echo 'Calculated checksum: '$(sha1sum /bin/tini) &amp;&amp; \
    chmod +x /bin/tini &amp;&amp; echo "$TINI_SHA  /bin/tini" | sha1sum -c 
    
ENTRYPOINT ["/bin/tini","--","/opt/nginx/docker-entrypoint.sh"]
ENTRYPOINT ["nginx", "-c"] 
CMD ["/etc/nginx/nginx.conf"]
</code></pre> 
<p>建议为了提高容器运行的可靠性，可以选择在打包镜像时加入tini的安装，并以tini作为启动入口。</p> 
<p>Jenkins的官方镜像中使用Tini 的使用</p> 
<p>首先，我们先简单聊聊Jenkins。当您运行Docker容器时，Docker会将它与系统的其他部分隔离开来。这种隔离发生在不同的级别（例如网络、文件系统、进程）。</p> 
<p>但Tini并不真正关注网络或文件系统，所以让我们把注意力放在Tini的一个重要概念上：进程。</p> 
<p>每个Docker容器都是一个PID命名空间，这意味着容器中的进程与主机上的其他进程是隔离的。PID命名空间是一棵树，从PID 1开始，通常称为init。</p> 
<p>注意：当你运行一个Docker容器时，镜像的ENTRYPOINT就是你的根进程，即PID 1（如果你没有ENTRYPOINT，那么CMD就会作为根进程，你可能配置了一个shell脚本，或其他的可执行程序，容器的根进程具体是什么，完全取决于你的配置）。</p> 
<p>与其他进程不同的是，PID 1有一个独特的职责，那就是收割“僵尸进程”。</p> 
<p>那何为“僵尸进程”呢？</p> 
<p>“僵尸进程”是指：</p> 
<ul><li>已经退出。</li><li>没有被其父进程wait（wait是指syscall父进程用于检索其子进程的退出代码）。</li><li>父进程已丢失（也就是说，它们的父进程已经不存在了)，这意味着他们永远不会被其父进程处理。</li></ul> 
<p>当“僵尸进程”被创建时（也就是说，一旦它的父进程非正常退出了，它也就跟着无法正常退出了），它会继承成为PID 1的子级，最后PID 1会负责关闭它。</p> 
<p>换句话说，有人必须在“不负责任”的父进程离开后，对这些“孤儿”进行清理，这是PID 1的作用。</p> 
<p>请注意，创建“僵尸进程”通常是不被允许的（也就是说，理想情况下，您应该修复代码，这样就不会创建“僵尸进程”），但是对于像Jenkins这种应用来说，它们是不可避免的：因为Jenkins通常运行的代码不是由Jenkins维护者编写的（也就是您的Jenkins构建脚本），所以他们也无法“修复代码”。</p> 
<p>这就是Jenkins使用Tini的原因：在构建了创建“僵尸进程”的脚本后进行清理。</p> 
<hr> 
<p>但其实Bash实际上也做同样的事情（收割“僵尸进程”），所以你可能会想：为什么不把Bash当作PID 1呢？</p> 
<p>第一个问题是，如果您将Bash作为PID 1运行，那么您发送到Docker容器的所有信号（例如，使用docker stop或docker kill）最终都会发送到Bash，Bash默认不会将它们转发到任何地方（除非您自己编写代码实现）。换句话说，如果你使用Bash来运行Jenkins，那么当你运行docker stop的时候，Jenkins将永远收不到停止信号！</p> 
<p>而Tini通过“信号转发”解决了这个问题：如果你向Tini发送信号，那么它也会向你的子进程发送同样的信号（在你的例子中是Jenkins）。</p> 
<p>第二个问题是，一旦您的进程退出，Bash也会继续退出。如果您不小心，Bash可能会退出，退出代码为0，而您的进程实际上崩溃了（但0表示“一切正常”；这将导致Docker重启策略不符合您的预期）。因为您真正想要的可能是Bash返回与您的进程相同的退出代码。</p> 
<p>请注意，您可以通过在Bash中创建信号处理程序来实际执行转发，并返回适当的退出代码来解决这个问题。另一方面，这需要做更多的工作，而添加Tini只是文档中的几行。</p> 
<hr> 
<p>其实还有另一个解决方案可以将Jenkins作为PID 1运行，即在Jenkins中添加另一个线程来负责收割“僵尸进程”。</p> 
<p>但这也不理想，原因有二：</p> 
<p>首先，如果将Jenkins以PID 1的身份运行，那么很难区分继承给Jenkins的进程（应该被收割）和Jenkins自己产生的进程（不应该被收割，因为还有其他代码已经在等待它们执行）。我相信你可以用代码来解决这个问题，但还是要问一遍：当你可以把Tini放进去的时候，为什么还要写呢？</p> 
<p>其次，如果Jenkins以PID 1运行，那么它可能不会接收到您发送的信号！</p> 
<p>这是PID 1进程中的微妙之处。与其他进程不同的是，PID 1没有默认的信号处理程序，这意味着如果Jenkins没有明确地为SIGTERM安装信号处理程序，那么该信号在发送时将被丢弃（而默认行为是终止该过程）。</p> 
<p>Tini确实安装了显式信号处理程序（顺便说一下，是为了转发信号），所以这些信号不再被丢弃。相反，它们被发送到Jenkins，Jenkins并不像PID 1（Tini ）那样运行，因此有默认的信号处理程序（注意：这不是Jenkins使用Tini的原因，Jenkins使用它来获取信号，但在RabbitMQ的镜像中是这个作用）。</p> 
<hr> 
<p>请注意，Tini中还有一些额外的功能，在Bash或Java中很难实现（例如，Tini可以注册为“子收割者”，因此它实际上不需要作为PID 1运行来完成“僵尸进程”收割工作），但是这些功能对于一些高级应用场景来说非常有用。</p> 
<p>希望以上内容对你有所帮助！</p> 
<p>如果您有兴趣了解更多，以下是一些可供参考的资料：</p> 
<ul><li>僵尸进程详解：https://blog.phusion.nl/2015/01/20/docker-and-the-pid-1-zombie-reaping-problem/</li><li>更简洁的解释：https://github.com/docker-library/official-images#init</li></ul> 
<p>最后，请注意Tini还有更多的选择（比如Phusion的基础镜像）。</p> 
<p>Tini的主要特性是：</p> 
<ul><li>做PID 1需要做的一切，而不做其他任何事情。像读取环境文件、改变用户、过程监控等事情不在Tini的范围内（还有其他更好的工具）；</li><li>零配置就能上手（如果运行不正常，Tini &gt;= 0.6也会警告您）；</li><li>它有丰富的测试。</li></ul>
                </div>
            </body>
            </html>
            